﻿using System.Globalization;
using System.Runtime.Serialization;

namespace OnDash.Mathematics.Data;

public struct Half : ISerializable, IComparable<Half>, IFormattable, IEquatable<Half>
{
    private ushort _bits;

    private const int MaxUlps = 1;

    public bool IsZero => _bits is 0 or 0x8000;
    public bool IsNan => (_bits & 0x7c00) == 0x7c00 && (_bits & 0x03ff) != 0x0000;

    public bool IsPositiveInfinity => _bits == 31744;
    public bool IsNegativeInfinity => _bits == 64512;

    public static readonly int SizeInBytes = 2;
    public const float MaxValue = 65504.0f;
    public const float MinValue = 5.96046448e-08f;
    public static readonly float MinNormalizedValue = 6.10351562e-05f;
    public static readonly float Epsilon = 0.00097656f;

    public unsafe Half(float f) : this()
    {
        _bits = SingleToHalf(*(int*)&f);
    }

    public Half(SerializationInfo info, StreamingContext context)
    {
        _bits = (ushort)info.GetValue("bits", typeof(ushort))!;
    }

    public Half(float f, bool throwOnError)
        : this(f)
    {
        if (!throwOnError) return;
        switch (f)
        {
            case > MaxValue:
                throw new ArithmeticException("Half: Positive maximum value exceeded.");
            case < -MaxValue:
                throw new ArithmeticException("Half: Negative minimum value exceeded.");
            case float.NaN:
                throw new ArithmeticException("Half: Inputs is not a number (NaN).");
        }

        if (float.IsPositiveInfinity(f))
            throw new ArithmeticException("Half: Inputs is positive infinity.");
        if (float.IsNegativeInfinity(f))
            throw new ArithmeticException("Half: Inputs is negative infinity.");
    }

    public Half(double d) : this((float)d)
    {
    }

    public Half(double d, bool throwOnError) : this((float)d, throwOnError)
    {
    }

    private static ushort SingleToHalf(int si32)
    {
        var sign = (si32 >> 16) & 0x00008000;
        var exponent = ((si32 >> 23) & 0x000000ff) - (127 - 15);
        var mantissa = si32 & 0x007fffff;

        switch (exponent)
        {
            case <= 0 and < -10:
                return (ushort)sign;
            case <= 0:
            {
                mantissa = mantissa | 0x00800000;
                var t = 14 - exponent;
                var a = (1 << (t - 1)) - 1;
                var b = (mantissa >> t) & 1;

                mantissa = (mantissa + a + b) >> t;

                return (ushort)(sign | mantissa);
            }
            case 0xff - (127 - 15) when mantissa == 0:
                return (ushort)(sign | 0x7c00);
            case 0xff - (127 - 15):
                mantissa >>= 13;
                return (ushort)(sign | 0x7c00 | mantissa | (mantissa == 0 ? 1 : 0));
            default:
            {
                mantissa = mantissa + 0x00000fff + ((mantissa >> 13) & 1);

                if ((mantissa & 0x00800000) != 0)
                {
                    mantissa = 0;
                    exponent += 1;
                }

                if (exponent > 30)
                    throw new ArithmeticException("Half: Hardware floating-point overflow.");

                return (ushort)(sign | (exponent << 10) | (mantissa >> 13));
            }
        }
    }

    public unsafe float ToSingle()
    {
        var i = HalfToFloat(_bits);
        return *(float*)&i;
    }

    private int HalfToFloat(ushort ui16)
    {
        var sign = (ui16 >> 15) & 0x00000001;
        var exponent = (ui16 >> 10) & 0x0000001f;
        var mantissa = ui16 & 0x000003ff;

        switch (exponent)
        {
            case 0 when mantissa == 0:
                return sign << 31;
            case 0:
            {
                while ((mantissa & 0x00000400) == 0)
                {
                    mantissa <<= 1;
                    exponent -= 1;
                }

                exponent += 1;
                mantissa &= ~0x00000400;
                break;
            }
            case 31 when mantissa == 0:
                return (sign << 31) | 0x7f800000;
            case 31:
                return (sign << 31) | 0x7f800000 | (mantissa << 13);
        }

        exponent = exponent + (127 - 15);
        mantissa = mantissa << 13;

        return (sign << 31) | (exponent << 23) | mantissa;
    }

    public static Half Parse(string s) => (Half)float.Parse(s);

    public static Half Parse(string s, NumberStyles style, IFormatProvider provider)
        => (Half)float.Parse(s, style, provider);

    public static bool TryParse(string s, out Half result)
    {
        float f;
        var b = float.TryParse(s, out f);
        result = (Half)f;
        return b;
    }

    public static byte[] GetBytes(Half h) => BitConverter.GetBytes(h._bits);

    public static Half FromBytes(byte[] value, int startIndex)
    {
        Half h;
        h._bits = BitConverter.ToUInt16(value, startIndex);
        return h;
    }

    public static bool TryParse(string s, NumberStyles style, IFormatProvider provider, out Half result)
    {
        float f;
        var b = float.TryParse(s, style, provider, out f);
        result = (Half)f;
        return b;
    }

    public void FromBinaryStream(BinaryReader bin)
    {
        _bits = bin.ReadUInt16();
    }

    public void ToBinaryStream(BinaryWriter bin)
    {
        bin.Write(_bits);
    }

    public static explicit operator Half(float f) => new(f);
    public static explicit operator Half(double d) => new(d);
    public static implicit operator float(Half h) => h.ToSingle();
    public static implicit operator double(Half h) => h.ToSingle();

    public int CompareTo(Half other) => ((float)this).CompareTo(other);

    public bool Equals(Half other)
    {
        short aInt, bInt;
        unchecked
        {
            aInt = (short)other._bits;
        }

        unchecked
        {
            bInt = (short)_bits;
        }

        if (aInt < 0)
            aInt = (short)(0x8000 - aInt);

        if (bInt < 0)
            bInt = (short)(0x8000 - bInt);

        var intDiff = MathHelper.Abs((short)(aInt - bInt));

        return intDiff <= MaxUlps;
    }

    public static bool operator ==(Half left, Half right) => left.Equals(right);
    public static bool operator !=(Half left, Half right) => !left.Equals(right);

    public override string ToString() => ToSingle().ToString(CultureInfo.InvariantCulture);

    public string ToString(string? format, IFormatProvider? formatProvider)
    {
        FormattableString formattable = $"";
        return formattable.ToString(formatProvider);
    }

    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        info.AddValue("bits", this._bits);
    }

    public override bool Equals(object? obj)
        => obj is Half half && Equals(half);

    public override int GetHashCode() => _bits.GetHashCode();
}